feat(control-plane): deliver a bootstrap secret into hosted tenant containers - #8253
Conversation
…ntainers (#8202) Resolves push-vs-pull: a second stub.start() call from injectSecrets can't reliably deliver a live secret (Cloudflare Containers only apply envVars at an actual cold boot, and createContainer already owns the tenant's one real start() call). Instead, provisionTenant now runs database -> secrets -> container, so injectSecrets' one-time exchange secret (previously discarded) rides the container's own cold-boot envVars as a bootstrap credential; the container exchanges it for the real custodied value via the new fetchBrokeredStoredSecret client against the broker's already-wired stored-secret path. Scoped to the mechanism + ORB (which reuses its unmodified self-host broker-client code for free); AMS's container-side wiring is a real separate lift and follow-up issue #8246.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
1 similar comment
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8253 +/- ##
==========================================
- Coverage 92.12% 91.60% -0.53%
==========================================
Files 782 782
Lines 78531 78585 +54
Branches 23720 23728 +8
==========================================
- Hits 72347 71986 -361
- Misses 5062 5524 +462
+ Partials 1122 1075 -47
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-23 15:06:10 UTC
Review summary Blockers
Nits — 5 non-blocking
Concerns raised — review before merging
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
… bootstrap (#8246) (#8263) Adds tenant-credential-resolution.ts, a duplicated (not imported -- cross- package import from packages/loopover-miner into root src/ fails tsc with TS6059, confirmed) exchange of LOOPOVER_TENANT_SECRET_TOKEN against the broker, mirroring src/orb/broker-client.ts's fetchBrokeredStoredSecret for ORB. hosted-entry.ts resolves it once per wake, best-effort -- no code in this package consumes the resolved value yet (the miner's stores are unconditionally local SQLite), so this proves the mechanism is wired for AMS without making a scheduled cycle fragile against an unused value, mirroring #8202/#8253's own "prove it works, defer consumption" precedent for ORB.
Summary
Resolves #8202 for its ORB scope (AMS split into #8246, see Boundaries below).
control-plane/src/secret-driver.tscould custody a tenant's secret in the broker but had no way to actually deliver it into a running container -- its own header called this "separate, not-yet-built infrastructure." This PR builds that delivery mechanism.Why not push the real secret value? I read the real
@cloudflare/containersSDK (not just this repo's own hand-rolled test interface) before picking a mechanism.createTenantContainercallsstub.start()exactly once, guarded byisProvisioned()-- there's no second call forinjectSecretsto hook into as the issue's push option suggested. And a repeatstart()against an already-running/starting container is a no-op or throws at the SDK level (envVarsare only ever applied at an actual cold boot). So a literal "push the DB connection string through a secondstub.start()call" isn't just worse than pull, it doesn't reliably work at all.What this ships instead (pull, bootstrapped by a small reorder):
provisionTenantnow runsprovisionDatabase -> injectSecrets -> createContainer(wascreateContainerfirst) so a bootstrap credential exists before the container's one real cold-bootstart()call.injectTenantSecrets(secret-driver.ts) stops discarding the one-time exchangesecretthe broker already hands back on custody -- it's returned asbootstrapSecretand threaded byprovisionTenantinto the very nextcreateContainercall.createTenantContainer(container-driver.ts) delivers it asLOOPOVER_TENANT_SECRET_TOKEN, merged alongside the existingLOOPOVER_PINNED_VERSIONinto oneenvVarsobject. Product-agnostic naming (noORB_/AMS_prefix), matchingPINNED_VERSION_ENV_VAR's own precedent -- bothOrbTenantContainerandAmsTenantContainerwill read the identical var name.fetchBrokeredStoredSecret(src/orb/broker-client.ts) lets a container exchange that bootstrap credential against the broker's already-wiredPOST /v1/orb/tokenstored-secret path (brokerOrbToken'sORB_SECRET_TYPE_TENANT_DB_CREDENTIALbranch,resolveStoredSecret-- already shipped server-side, just never had a client).OrbTenantContainerruns the unmodified self-host image, so ORB gets this for free once wired; AMS has no broker-aware code today, hence the split.deprovisionTenant's own order is deliberately left unchanged -- "revoke the secret before the DB/container it belonged to is gone" is an independent security property, not a mechanical requirement to mirror provision's new order.Tests
test/integration/orb-broker.test.ts: new test proving a revokedtenant_db_credentialenrollment's secret can no longer be exchanged -- the exact "revocation actually removes access, not just broker custody" deliverable Deliver secrets/env vars into a running hosted tenant container (OrbTenantContainer/AmsTenantContainer) #8202 called for. (The existing revocation test used the GitHub-token enrollment type; this is the missing stored-secret combination.)test/unit/orb-broker-client.test.ts: full coverage offetchBrokeredStoredSecret(success, custom broker URL, shared URL-safety validation, non-OK response, missingsecretValue, both??defensive branches).control-plane/test/container-driver.test.ts: bootstrap-secret-only, both-pinned-and-bootstrap-merged, neither-set (byte-identical to pre-Build a fleet rollout mechanism #4898), and idempotent-repeat-create cases.control-plane/test/provisioning.test.ts: new step-order assertion, plus a test provingbootstrapSecretactually threads frominjectSecrets' result into thecreateContainerrequest (and an omission test for when the driver returns none).control-plane/test/secret-driver.test.ts,driver-factory.test.ts: updated to expect the now-returnedbootstrapSecret.control-plane/test/provisioning-pagerduty.test.ts: fixed a real bug the reorder surfaced -- itsdriverThatThrowsOnhelper's no-op stand-in forinjectSecretsreturnedundefined(aPromise<void>slipping pastinjectSecrets' real return type via TypeScript's void-return special-casing). Under the old create-container-first order this was never exercised forcreateContainer-throws tests; the new order runsinjectSecretsfirst, and its no-op return value gets destructured. Fixed to return a real{}.Verified: full unsharded
npm run test:coverage(20975/20996 passing, 0 failures),tsc --noEmit --incremental falseclean,control-plane's own build +node:testsuite (193/193) + real c8/lcov coverage (100% line+branch on every file this PR touches),git diff --checkclean.Boundaries
Scoped to the delivery mechanism + ORB, matching #8202's own explicit "needs a design decision, not a default extension" framing -- picking a mechanism was the hard part, and AMS's container-side wiring is genuinely new code (
packages/loopover-minerhas zero broker-aware code today), not a second call site for the same thing. Split into #8246 rather than bundled here.Does not wire a runtime consumer for the resolved secret value into
src/server.ts's boot sequence -- #8202's own deliverables are the delivery mechanism and proof it works (including revocation), not runtime consumption. That's #7858's hosted half, already listed as blocked-by #8202.Test plan
npm run typecheck(forced non-incremental)npm run test:coverage(full unsharded suite)control-plane:npm run build && npm run test:node(193/193)control-plane: real coverage vianpm run control-plane:coverage-- 100% line+branch on every touched filegit diff --check